<?php

namespace liketp\database\concern;

use Closure;
use liketp\database\Query;
use liketp\database\Raw;
use liketp\exception\Exception;

trait WhereBuilder
{
    protected function parseWhere($query, $tpl = 'WHERE %$%')
    {
        $wheres = $query->getOptions('where');
        $items = [];
        foreach ($wheres as $k => $where) {
            $item = $this->parseWhereItem($where, $query);
            if ($k > 0) $item = $where['logic'] . ' ' . $item;
            array_push($items, $item);
        }

        return $this->wrap(implode(' ', $items), true, $tpl);
    }

    protected function parseWhereItem(array $where, $query): string
    {
        $operate = $where['operate'];

        if ($this->isSimpleSymbol($operate)) {
            $slice = [$this->getWhereField($where), $operate, $this->getWhereValue($where, $query)];
        } else {
            $method = $this->operateMethod($operate);
            $slice = $this->$method($where, $query);
        }

        return implode(' ', $slice);
    }

    //------------------------------------------------ private
    private function isSimpleSymbol($operate)
    {
        $symbols = ['=', '<', '>', '<=', '>='];
        return in_array($operate, $symbols);
    }

    private function operateMethod($operate)
    {
        return 'fn' . ucfirst($operate);
    }

    private function getWhereField($where): string
    {
        $field = $where['field'];

        // query
        if ($field instanceof Query) {
            throw new Exception('please use parseQuery method to process sql field');
        }
        // raw
        else if ($field instanceof Raw) {
            return $field->content();
        }
        // string
        else if (is_string($field)) {

            return $this->backquote($field);
        }
        return '';
    }

    private function getWhereValue($where, $query)
    {
        $value = $where['value'];

        // query
        if ($value instanceof Query) {
            return $this->buildSelectSql($value);
        }
        // closure
        else if ($value instanceof Closure) {
            $newQuery = $query->createQuery();
            $value($newQuery);
            return $this->buildSelectSql($newQuery);
        }
        // raw
        else if ($value instanceof Raw) {
            return $value->content();
        }
        // string, int, float, bool, null
        else if (is_scalar($value) || is_null($value)) {

            $key = is_string($where['field']) ? $where['field'] : '';
            return $query->setBindData($key, $value);
        }
        // array
        else if (is_array($value)) {

            return $value;
        } else {
            throw new Exception('can\'t parse where value');
        }
    }

    private function getWhereBindKey($where, $default = '')
    {
        return is_string($where['field']) ? $where['field'] : $default;
    }


    //------------------------------------------------ methods to parse where operate
    private function fnQuery($where, $query)
    {
        $whereQuery = $where['field'];
        $sql = $this->parseWhere($whereQuery, '(%$%)');
        $query->mergeBindData($whereQuery->getBindData());
        return [$sql];
    }

    private function fnClosure($where, $query)
    {
        $newQuery = $query->createQuery($query);
        $fn = $where['field'];
        $fn($newQuery);

        $where = $this->parseWhere($newQuery, '(%$%)');
        $query->mergeBindData($newQuery->getBindData());

        return [$where];
    }

    private function fnRaw($where, $query)
    {
        $raw = $where['field'];
        $sqlPartment = $raw->content();
        $query->mergeBindData($raw->bindData());
        return [$sqlPartment];
    }

    private function fnLike($where, $query)
    {
        $field = $this->getWhereField($where);
        $value = $this->getWhereValue($where, $query);
        return [$field, 'like', $value];
    }

    private function fnIn($where, $query)
    {
        $valString = '';
        $field = $this->getWhereField($where);
        $value = $this->getWhereValue($where, $query);

        if (is_array($value)) {

            $key = $this->getWhereBindKey($where);
            $valueList = array_map(function ($v) use ($key, $query) {
                return $query->setBindData($key, $v);
            }, $value);

            $valString = implode(',', $valueList);
        } else {

            $value = trim($value, "()");
            $valString = $value;
        }

        return [$field, 'IN (', $valString, ')'];
    }

    private function fnBetween($where, $query)
    {
        $field = $this->getWhereField($where);
        $value = $this->getWhereValue($where, $query);

        if (!is_array($value) || count($value) != 2) {
            throw new Exception('the value of between is illegal');
        }

        [$min, $max] = $value;
        if ($min > $max) [$min, $max] = [$max, $min];

        $key = $this->getWhereBindKey($where);
        $min = $query->setBindData($key, $min);
        $max = $query->setBindData($key, $max);

        return ['(', $min, '<=', $field, 'and', $field, '<=', $max, ')'];
    }

    private function fnNull($where)
    {
        $field = $this->getWhereField($where);
        $operate = $where['addition']['opposite'] ? 'IS NOT NULL' : 'IS NULL';
        return [$field, $operate];
    }

    private function fnExp($where)
    {
        $field = $this->getWhereField($where);
        $condition = $where['value'];
        if ($condition instanceof Raw) $condition = $condition->content();
        return [$field, $condition];
    }

    private function fnTime($where, $query) {}

    private function fnExists($where)
    {
        $condition = $where['field'];
        $opposite = $where['addition']['opposite'];

        if ($condition instanceof Query) {
            //$sql = $this->parseWhere($condition, '(%$%)');
            //$query->mergeBindData($whereQuery->getBindData());
            //not done
            $sql = $this->parseQuery($condition);
        } else if ($condition instanceof Raw) {
            $sql = $condition->content();
        } else {
            throw new Exception('parse query options [exists] failed.');
        }

        return [($opposite ? 'NOT ' : '') . 'EXISTS (', $sql . ')'];
    }

    private function fnColumn($where)
    {
        $field1 = $where['field'];
        [$operate, $field2] = $where['value'];
        $field1 = $this->backquote($field1);
        $field2 = $this->backquote($field2);
        return [$field1, $operate, $field2];
    }
}
